渐进式视觉对齐:用视觉反馈把 UI 迁移到任何渲染器
过去的 UI 协作,设计师先出图形化设计稿。
工程师再把设计稿翻译成代码。
设计师验收,发现间距、字号、对齐、状态不一致,再让工程师改。
流程经常变成:
设计师画图 -> 工程师还原 -> 设计师验收 -> 工程师修改 -> 设计师再验收 -> 继续反复
这里的损耗来自一次翻译:
图形稿 -> 可运行 UI
图形稿给人看。
生产代码给机器执行。
工程师夹在中间,把视觉意图翻译成可执行 UI。
很多来回沟通,都在修这次翻译的误差。
比如 padding 差 2px。
比如价格区字号小了一点。
AI 出现后,这条链路开始变化。
设计师可以直接指挥 AI 编写组件样式代码,在真机上看结果。
不满意,就继续让 AI 改。
直到自己看满意。
这一步先解决新增 UI。
接下来的问题是存量 UI。
存量页面已经在线上运行。
里面混着业务逻辑、接口数据、实验分支、设备环境和历史补丁。
它没有一份干净的静态组件代码。
再往外推,问题会变得更大。
AI 设计工具、传统设计工具、页面搭建工具,都可以产出 HTML。
HTML 能运行,能截图,能作为视觉参照。
那它也应该能被迁移到 RN、小程序、Harmony、Native,或者任何目标渲染器。
所以这篇文章讨论的主线是:
已有可运行视觉参照 -> 目标渲染器代码 -> 截图反馈 -> 渐进式收敛 -> 视觉一致
一、先让新 UI 直接变成可运行代码
设计验收前移
设计师直接生产可运行 UI 后,设计验收会前移。
过去设计师等工程师实现后验收 UI。
现在设计师在出稿阶段完成第一轮真机验收。
工程师拿到的也不再是一张需要还原的图。
工程师拿到的是静态组件代码。
这份代码已经运行过。
已经调过。
已经被设计师自己验收过。
AI 再把这份代码转写、搬运、接入生产仓库。
原来最痛的“设计稿还原”问题,被绕开了一大截。
样式层从业务层拆出来
这条新流程要求前端组件拆成两层。
视觉层关心布局、字号、颜色、间距、圆角、动画和基础状态。
业务层关心数据、埋点、实验、状态和生产接入。
设计师静态组件仓库只承载视觉层。
没有网络请求。
没有埋点。
没有复杂业务状态。
它更像一个干净的视觉函数:
VisualUI = f(props)
生产仓库继续承载完整业务运行时:
Business Runtime = 数据 + 状态 + 埋点 + 实验 + 生产约束
设计师在静态组件仓库里把视觉调对。
工程师在生产仓库里把业务接对。
前端工程师的工作重心随之变化。
他不再主要负责把图形稿翻译成页面。
他负责把已经被设计师验证过的视觉代码,接入真实业务运行时。
二、存量页面要先抓住一个状态切片
增量组件已经跑通
对于增量组件,这条链路很好跑。
设计师在静态组件仓库里新建页面。
让 AI 编码商品卡、货架、轮播卡,或者某个业务互动玩法的基础视觉和简单交互。
设计师在真机上预览,自己把样式调到满意。
工程师再把这份样式代码接进生产仓库。
这条链路解决新增组件。
存量组件的问题还没有解决。
存量页面缺少静态切面
存量组件没有设计师自己验证过的静态组件代码。
只有线上生产环境运行中的代码。
这份代码很难直接交给设计师改。
它里面混着样式、数据、埋点、实验、状态和历史补丁。
第一反应是让 AI 从生产代码里把样式剥离出来,转写到静态组件仓库。
AI 很会搬代码,也很会转写。
但这一步经常对不齐。
因为生产组件的最终渲染结果,由很多东西一起算出来。
最终视觉 = 样式代码 + 输入数据 + 实验分支 + 设备环境 + 接口返回 + 运行时状态
AI 只读源码,只能抽到一段代码。
它不知道这段代码在真实运行环境里最后长什么样。
所以存量页面要先固定一个当前正在运行的状态切片。
蒸馏对象:线上状态切片
蒸馏结果:静态视觉快照
修改对象:静态视觉快照
回灌目标:生产代码在同一状态切片下的渲染结果
验收方式:截图 diff + 人类确认
记录最终渲染输入
状态切片的数据来源可能很复杂。
有的来自当前接口。
有的来自很早之前的接口。
有的来自全局变量。
有的来自实验系统。
有的来自本地缓存。
追这些来源会很痛苦。
更直接的方式是记录最终渲染输入。
在组件 return 前,把当前 JSX 用到的文本、图片、flag、样式参数和分支结果打出来。
这时关心的已经不是数据怎么来的。
关心的是当前这一帧到底用什么渲染。
再进一步,可以抓运行时 UI 树。
这里要抓最终 host UI tree。
也就是已经展开到 View、Text、Image、ScrollView、Pressable 这一层的树。
自定义组件已经被展开。
分支已经被选择。
文本已经确定。
样式 props 已经落到节点上。
如果同一台设备、同一套字体、同一个渲染器都被锁住,这份 UI 树就能生成很接近的静态快照。
它绕过了复杂数据来源。
它把当前状态切片直接变成可复现的视觉结构。
但它还不够完成整件事。
静态快照要靠截图 diff 验收。
生产回灌要靠源码映射。
所以更完整的产物应该包含两份东西:
ui-tree.json:复现当前视觉快照
source-map.json:记录节点对应的源码位置和组件路径
前者负责让设计师能改。
后者负责把修改写回生产仓库。
三、反馈闭环让它可收敛
截图不能只交给 AI 判断
存量页面抽出来之后,要拿到两张运行截图。
线上截图 = 线上页面状态切片
复现截图 = 静态快照渲染结果
AI 可以看截图。
但截图只交给 AI 判断,稳定性不够。
AI 是概率模型,看截图主要做语义匹配。
它不擅长稳定处理 1px、字号、基线、圆角和间距。
所以截图要变成脚本能测量的反馈信号。
把视觉差异变成测量信号
最小可行的办法很朴素。
选中一个要复现的组件。
在生产页面里,把这个组件临时加上大红色背景。
在静态复现页里,也把对应组件加上大红色背景。
然后分别截图。
这时问题变成:
让两张截图里的红色区域尽量重合。
这个目标很干净。
它不依赖 DOM。
不依赖组件树。
只依赖用户真实看到的截图像素。
脚本从两张图里提取红色 mask。
然后计算红色区域的重合程度。
线上红色区域和复现红色区域,重合越大越好,不重合越小越好。
只给一个总 loss 还不够。
总 loss 只能告诉 AI 差多少。
它不能稳定告诉 AI 往哪改。
所以脚本要继续拆出有方向的信号。
dx / dy:红色区域重心的水平和垂直偏移
dw / dh:红色区域水平投影和垂直投影的尺寸差
area:红色面积差
xorLoss:最终不重合面积
这样反馈就很直接。
当前红色区域比目标偏右 8px,就向左调。
当前红色区域比目标矮 4px,就增高。
当前红色区域面积偏大,就收缩尺寸或者检查形状。
重心提供位置锚点。
投影提供尺寸信号。
面积提供整体占用信号。
xorLoss 提供最终验收信号。
完整闭环变成:
生产页组件染红 -> 静态快照组件染红 -> 双截图 -> 脚本提取红色 mask -> 计算 dx/dy/dw/dh/area/xorLoss -> AI 改样式 -> 再截图 -> 继续收敛
把截图转成脚本反馈信号,才能引导像素级收敛。
从骨架到细节逐层冻结
整页一次性拟合,自由度太高。
外层骨架错了,里面所有像素 diff 都是噪声。
这个还原过程要从低频到高频。
先定页面骨架。
再定大区块。
再定组件结构。
再定元素几何。
最后抠颜色、字号、圆角、阴影和 1px 细节。
页面骨架 -> 大区块 -> 组件结构 -> 元素几何 -> 视觉细节
每一层只开放这一层的动作空间。
这一层收敛后冻结。
然后进入下一层。
UI 还原真正需要设计的,是这个可收敛的搜索空间。
直接学习最终截图太难。
最终截图里混着骨架、区块、组件、文字、图片、颜色和阴影。
它是一个高噪声输出。
更稳定的方式,是控制参照页面,逐层暴露中间结构。
先保留最外层骨架,把里面清空,填明显占位。
静态快照先复现骨架。
骨架对齐后冻结。
参照页面再恢复一个区块。
静态快照再复现这个区块。
区块对齐后继续冻结。
然后再恢复组件、元素和细节。
这个过程很像模型蒸馏。
传统蒸馏会学习原模型的中间表征。
这里也一样。
最终截图 = 高噪声输出
骨架截图 = 中间表征
区块截图 = 中间表征
组件截图 = 中间表征
所以这个方法叫渐进式 UI 蒸馏:
以可运行 UI 为参照,以目标渲染器代码为结果,通过运行时截图、结构化 diff 和逐层冻结,把真实视觉层蒸馏成可继续修改的 UI 代码。
四、从线上页面推到任意渲染器
HTML 是通用入口
这套方法继续往外推,就会覆盖所有设计工具。
尤其是 AI 设计工具。
这些工具最后大概率都会产出 HTML。
HTML 是最通用的可运行 UI 格式。
它能被浏览器渲染。
它能被截图。
它能作为视觉参照。
接下来问题变成:
把这份 HTML 的渲染结果,迁移到目标渲染器。
目标渲染器可以是小程序。
可以是 RN。
可以是 Harmony。
也可以是 Native。
第一版转写可以很粗。
AI 先根据 HTML 生成目标端代码。
目标端跑起来。
两边截图。
脚本测量差异。
AI 再改目标端代码。
这条链路天然可调试:
设计工具导出 HTML -> 浏览器渲染参照 -> 目标渲染器初稿 -> 双截图 diff -> AI 修改目标代码 -> 再渲染 -> 继续收敛
以前“导出代码”经常是一次性动作。
导得好就能用。
导得差就废掉。
这里的关键变化是,导出之后还有反馈闭环。
它可以一步步调。
可以先对骨架。
再对区块。
再对组件。
再对元素。
最后对颜色、字号、圆角和 1px 细节。
异构渲染器也能收敛
人类做 UI 迁移,本来就是这样。
看一个参照。
在另一个平台里实现。
发现哪里不对。
继续修。
直到看起来一致。
区别在于,过去这个闭环靠人眼。
现在可以拆成机器流程:
人给目标 -> AI 改代码 -> 渲染器出图 -> 脚本量差异 -> AI 继续改
同构场景更快。
比如 RN 页面抽成静态 RN 快照。
第一次转写可能已经接近 90%。
异构场景更慢。
比如 HTML 迁移到小程序,或者 HTML 迁移到 Native。
第一次转写可能差很多。
但只要目标端能渲染,能截图,能被测量,就可以继续收敛。
所有 UI 最后都会落到屏幕像素。
视觉一致性可以被测量。
能测量,就能优化。
所以这套方法最终解决的范围会扩大。
它可以从线上页面抽静态快照。
也可以把任意可运行 UI,渐进式迁移到任意目标渲染器。
任意可运行 UI -> 任意目标渲染器 -> 截图 diff -> 渐进式收敛
五、UI 一致性工作台
线上页面修改链路
最终这件事不应该停在脚本里。
它应该变成一个线上工具。
这个工具可以叫 UI 一致性工作台。
进入线上页面 -> 选择组件 -> 点击蒸馏 -> 生成静态快照 -> 自然语言修改 -> 回灌生产并反向校准 -> 三端预览确认
使用者打开平台。
平台分配一台真机,实时串流线上生产页面。
使用者进入某个页面。
看到某个组件不舒服,就点击蒸馏。
平台再启动另一台同型号真机。
一边跑线上页面。
一边跑静态快照。
系统从骨架开始,一层一层还原这个页面切片。
骨架对齐。
区块对齐。
组件对齐。
元素对齐。
最后抠颜色、字号、圆角和细节。
蒸馏完成后,使用者拿到一个静态快照。
这个快照没有业务逻辑。
没有埋点。
没有实验。
只有当前页面状态下的视觉层代码。
使用者直接用自然语言改它。
比如:
这个价格区再紧一点。
按钮颜色换成业务主色。
商品图和标题之间留白小一点。
这个卡片整体再轻一点。
AI 修改静态快照。
使用者在真机上实时看结果。
设计工具接入链路
UI 一致性工作台也可以接设计工具。
使用者在任意设计工具里生成页面。
导出 HTML。
平台用浏览器跑这份 HTML。
把浏览器截图作为参照。
然后选择目标渲染器。
比如 RN。
比如小程序。
比如 Harmony。
比如 Native。
平台生成目标端初稿。
跑目标端。
截图。
diff。
修代码。
继续收敛。
流程变成:
设计工具生成 UI -> 导出 HTML -> 浏览器参照截图 -> 选择目标渲染器 -> 生成目标端代码 -> 截图 diff -> 渐进式校准 -> 得到目标端 UI
这样所有能导出 HTML 的工具,都能接进来。
AI 设计工具生成的是视觉参照。
UI 一致性工作台负责把这个参照迁移到真实业务渲染器。
回灌生产也要反向校准
确认静态快照后,平台把这次样式修改 apply 回生产仓库。
但回灌不能只靠 AI 转写。
静态快照改完后,生产仓库又会发生一次翻译。
所以回灌也要走同一套反馈闭环。
平台把修改 apply 回生产仓库后,重新运行生产页面。
再截图。
这次目标图变成修改后的静态快照。
生产页面截图变成待校准结果。
系统继续用同一套 diff 信号校准:
红色 mask -> dx/dy -> dw/dh -> area -> xorLoss -> AI 修生产代码 -> 再截图
直到生产仓库渲染结果对齐静态快照。
然后平台自动拉起 iOS、Android、Harmony 三端。
三端都进入同一个页面状态。
三端都渲染修改后的生产页面。
使用者最后确认三端效果。
这时修改才算完成。
所以蒸馏阶段也应该同时观察三端。
静态快照要在三端都接近原始参照。
修改后也要在三端都符合预期。
最后这个工具的边界很清楚:
线上页面状态 -> 蒸馏成静态快照 -> 修改 -> 回灌生产 -> 三端反向校准
设计工具 HTML -> 迁移到目标渲染器 -> 渐进式校准 -> 接入生产
它服务的不只是设计师。
产品、运营、设计师、前端工程师,甚至任何看到 UI 不舒服的人,都可以进入这个工作流。
他们不需要先理解完整生产仓库。
他们只需要选中一个可运行参照,然后让系统把它变成可修改、可回灌、可校准的 UI 代码。